用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/diegosouzapw/awesome-omni-skill --skill ash命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | ash |
| description | Rules for working with Ash |
Ash is an opinionated, composable framework for building applications in Elixir. It provides a declarative approach to modeling your domain with resources at the center. Read documentation before attempting to use its features. Do not assume that you have prior knowledge of the framework or its conventions.
Use code interfaces on domains to define the contract for calling into Ash resources. See the Code interface guide for more.
Define code interfaces on the domain, like this:
resource ResourceName do
define :fun_name, action: :action_name
end
For more complex interfaces with custom transformations:
define :custom_action do
action :action_name
args [:arg1, :arg2]
custom_input :arg1, MyType do
transform do
to :target_field
using &MyModule.transform_function/1
end
end
end
Prefer using the primary read action for "get" style code interfaces, and using get_by when the field you are looking up by is the primary key or has an identity on the resource.
resource ResourceName do
define :get_thing, action: :read, get_by: [:id]
end
Avoid direct Ash calls in web modules - Don't use Ash.get!/2 and Ash.load!/2 directly in LiveViews/Controllers, similar to avoiding Repo.get/2 outside context modules:
You can also pass additional inputs in to code interfaces before the options:
resource ResourceName do
define :create, action: :action_name, args: [:field1]
end
Domain.create!(field1_value, %{field2: field2_value}, actor: current_user)
You should generally prefer using this map of extra inputs over defining optional arguments.
# BAD - in LiveView/Controller
group = MyApp.Resource |> Ash.get!(id) |> Ash.load!(rel: [:nested])
# GOOD - use code interface with get_by
resource DashboardGroup do
define :get_dashboard_group_by_id, action: :read, get_by: [:id]
end
# Then call:
MyApp.Domain.get_dashboard_group_by_id!(id, load: [rel: [:nested]])
Code interface options - Prefer passing options directly to code interface functions rather than building queries manually:
# PREFERRED - Use the query option for filter, sort, limit, etc.
# the query option is passed to `Ash.Query.build/2`
posts = MyApp.Blog.list_posts!(
query: [
filter: [status: :published],
sort: [published_at: :desc],
limit: 10
],
load: [author: :profile, comments: [:author]]
)
# All query-related options go in the query parameter
users = MyApp.Accounts.list_users!(
query: [filter: [active: true], sort: [created_at: :desc]],
load: [:profile]
)
# AVOID - Verbose manual query building
query = MyApp.Post |> Ash.Query.filter(...) |> Ash.Query.load(...)
posts = Ash.read!(query)
Supported options: load:, query: (which accepts filter:, sort:, limit:, offset:, etc.), page:, stream?:
Using Scopes in LiveViews - When using Ash.Scope, the scope will typically be assigned to scope in LiveViews and used like so:
# In your LiveView
MyApp.Blog.create_post!("new post", scope: socket.assigns.scope)
Inside action hooks and callbacks, use the provided context parameter as your scope instead:
|> Ash.Changeset.before_transaction(fn changeset, context ->
MyApp.ExternalService.reserve_inventory(changeset, scope: context)
changeset
end)
For each action defined in a code interface, Ash automatically generates corresponding authorization check functions:
can_action_name?(actor, params \\ %{}, opts \\ []) - Returns true/false for authorization checkscan_action_name(actor, params \\ %{}, opts \\ []) - Returns {:ok, true/false} or {:error, reason}Example usage:
# Check if user can create a post
if MyApp.Blog.can_create_post?(current_user) do
# Show create button
end
# Check if user can update a specific post
if MyApp.Blog.can_update_post?(current_user, post) do
# Show edit button
end
# Check if user can destroy a specific comment
if MyApp.Blog.can_destroy_comment?(current_user, comment) do
# Show delete button
end
These functions are particularly useful for conditional rendering of UI elements based on user permissions.
Ash.Changeset.after_action/2, Ash.Changeset.before_action/2 to add additional logic
inside the same transaction.Ash.Changeset.after_transaction/2, Ash.Changeset.before_transaction/2 to add additional logic
outside the transaction.where clauses for conditional executiononly_when_valid? to skip preparations when the query is invalidAsh moduleUse Ash.Query to build queries for reading data from your resources. The query module provides a declarative way to filter, sort, and load data.
Important: You must require Ash.Query if you want to use Ash.Query.filter/2, as it is a macro.
If you see errors like the following:
Ash.Query.filter(MyResource, id == ^id)
error: misplaced operator ^id
The pin operator ^ is supported only inside matches or inside custom macros...
iex(3)> Ash.Query.filter(MyResource, something == true)
error: undefined variable "something"
└─ iex:3
You are very likely missing a require Ash.Query
Ash.Query.filter(query, field == value)Ash.Query.sort(query, field: :asc)Ash.Query.load(query, [:author, :comments])Ash.Query.limit(query, 10)Ash.Query.offset(query, 20)Functions to call actions, like Ash.create and code interfaces like MyApp.Accounts.register_user all return ok/error tuples. All have ! variations, like Ash.create! and MyApp.Accounts.register_user!. Use the ! variations when you want to "let it crash", like if looking something up that should definitely exist, or calling an action that should always succeed. Always prefer the raising ! variation over something like {:ok, user} = MyApp.Accounts.register_user(...).
All Ash code returns errors in the form of {:error, error_class}. Ash categorizes errors into four main classes:
Ash.Error.Forbidden) - Occurs when a user attempts an action they don't have permission to performAsh.Error.Invalid) - Occurs when input data doesn't meet validation requirementsAsh.Error.Framework) - Occurs when there's an issue with how Ash is being usedAsh.Error.Unknown) - Occurs for unexpected errors that don't fit the other categoriesThese error classes help you catch and handle errors at an appropriate level of granularity. An error class will always be the "worst" (highest in the above list) error class from above. Each error class can contain multiple underlying errors, accessible via the errors field on the exception.
Validations ensure that data meets your business requirements before it gets processed by an action. Unlike changes, validations cannot modify the changeset - they can only validate it or add errors.
Validations work on both changesets and queries. Built-in validations that support queries include:
action_is, argument_does_not_equal, argument_equals, argument_incompare, confirm, match, negate, one_of, present, string_lengthsupports/1 callbackCommon validation patterns:
# Built-in validations with custom messages
validate compare(:age, greater_than_or_equal_to: 18) do
message "You must be at least 18 years old"
end
validate match(:email, "@")
validate one_of(:status, [:active, :inactive, :pending])
# Conditional validations with where clauses
validate present(:phone_number) do
where present(:contact_method) and eq(:contact_method, "phone")
end
# only_when_valid? - skip validation if prior validations failed
validate expensive_validation() do
only_when_valid? true
end
# Action-specific vs global validations
actions do
create :sign_up do
validate present([:email, :password]) # Only for this action
end
read :search do
argument :email, :string
validate match(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/) # Validates query arguments
end
end
validations do
validate present([:title, :body]), on: [:create, :update] # Multiple actions
end
Create custom validation modules for complex validation logic:
defmodule MyApp.Validations.UniqueUsername do
use Ash.Resource.Validation
@impl true
def init(opts), do: {:ok, opts}
@impl true
def validate(changeset, _opts, _context) do
# Validation logic here
# Return :ok or {:error, message}
end
end
# Usage in resource:
validate {MyApp.Validations.UniqueUsername, []}
Make validations atomic when possible to ensure they work correctly with direct database operations by implementing the atomic/3 callback in custom validation modules.
Preparations modify queries before they're executed. They are used to add filters, sorts, or other query modifications based on the query context.
Common preparation patterns:
# Built-in preparations
prepare build(sort: [created_at: :desc])
prepare build(filter: [active: true])
# Conditional preparations with where clauses
prepare build(filter: [visible: true]) do
where argument_equals(:include_hidden, false)
end
# only_when_valid? - skip preparation if prior validations failed
prepare expensive_preparation() do
only_when_valid? true
end
# Action-specific vs global preparations
actions do
read :recent do
prepare build(sort: [created_at: :desc], limit: 10)
end
end
preparations do
prepare build(filter: [deleted: false]), on: [:read, :update]
end
defmodule MyApp.Validations.IsEven do
# transform and validate opts
use Ash.Resource.Validation
@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
# This is optional, but useful to have in addition to validation
# so you get early feedback for validations that can otherwise
# only run in the datalayer
def validate(changeset, opts, _context) do
value = Ash.Changeset.get_attribute(changeset, opts[:attribute])
if is_nil(value) || (is_number(value) && rem(value, 2) == 0) do
:ok
else
{:error, field: opts[:attribute], message: "must be an even number"}
end
end
@impl true
def atomic(changeset, opts, context) do
{:atomic,
# the list of attributes that are involved in the validation
[opts[:attribute]],
# the condition that should cause the error
# here we refer to the new value or the current value
expr(rem(^atomic_ref(opts[:attribute]), 2) != 0),
# the error expression
expr(
error(^InvalidAttribute, %{
field: ^opts[:attribute],
# the value that caused the error
value: ^atomic_ref(opts[:attribute]),
# the message to display
message: ^(context.message || "%{field} must be an even number"),
vars: %{field: ^opts[:attribute]}
})
)
}
end
end
# WRONG - redundant validation
attribute :name, :string do
allow_nil? false
constraints min_length: 1
end
validate present(:name) do # Redundant! allow_nil? false already handles this
message "Name is required"
end
validate attribute_does_not_equal(:name, "") do # Redundant! min_length: 1 already handles this
message "Name cannot be empty"
end
# CORRECT - let attribute constraints handle basic validation
attribute :name, :string do
allow_nil? false
constraints min_length: 1
end
Changes allow you to modify the changeset before it gets processed by an action. Unlike validations, changes can manipulate attribute values, add attributes, or perform other data transformations.
Common change patterns:
# Built-in changes with conditions
change set_attribute(:status, "pending")
change relate_actor(:creator) do
where present(:actor)
end
change atomic_update(:counter, expr(^counter + 1))
# Action-specific vs global changes
actions do
create :sign_up do
change set_attribute(:joined_at, expr(now())) # Only for this action
end
end
changes do
change set_attribute(:updated_at, expr(now())), on: :update # Multiple actions
change manage_relationship(:items, type: :append), on: [:create, :update]
end
Create custom change modules for reusable transformation logic:
defmodule MyApp.Changes.SlugifyTitle do
use Ash.Resource.Change
def change(changeset, _opts, _context) do
title = Ash.Changeset.get_attribute(changeset, :title)
if title do
slug = title |> String.downcase() |> String.replace(~r/[^a-z0-9]+/, "-")
Ash.Changeset.change_attribute(changeset, :slug, slug)
else
changeset
end
end
end
# Usage in resource:
change {MyApp.Changes.SlugifyTitle, []}
Create a change module with lifecycle hooks to handle complex multi-step operations:
defmodule MyApp.Changes.ProcessOrder do
use Ash.Resource.Change
def change(changeset, _opts, context) do
changeset
|> Ash.Changeset.before_transaction(fn changeset ->
# Runs before the transaction starts
# Use for external API calls, logging, etc.
MyApp.ExternalService.reserve_inventory(changeset, scope: context)
changeset
end)
|> Ash.Changeset.before_action(fn changeset ->
# Runs inside the transaction before the main action
# Use for related database changes in the same transaction
Ash.Changeset.change_attribute(changeset, :processed_at, DateTime.utc_now())
end)
|> Ash.Changeset.after_action(fn changeset, result ->
# Runs inside the transaction after the main action, only on success
# Use for related database changes that depend on the result
MyApp.Inventory.update_stock_levels(result, scope: context)
{changeset, result}
end)
|> Ash.Changeset.after_transaction(fn changeset,
{:ok, result} ->
# Runs after the transaction completes (success or failure)
# Use for notifications, external systems, etc.
MyApp.Mailer.send_order_confirmation(result, scope: context)
{changeset, result}
{:error, error} ->
# Runs after the transaction completes (success or failure)
# Use for notifications, external systems, etc.
MyApp.Mailer.send_order_issue_notice(result, scope: context)
{:error, error}
end)
end
end
# Usage in resource:
change {MyApp.Changes.ProcessOrder, []}
Prefer to put code in its own module and refer to that in changes, preparations, validations etc.
For example, prefer this:
defmodule MyApp.MyDomain.MyResource.Changes.SlugifyName do
use Ash.Resource.Change
def change(changeset, _, _) do
Ash.Changeset.before_action(changeset, fn changeset, _ ->
slug = MyApp.Slug.get()
Ash.Changeset.force_change_attribute(changeset, :slug, slug)
end)
end
end
change MyApp.MyDomain.MyResource.Changes.SlugifyName
Relationships describe connections between resources and are a core component of Ash. Define relationships in the relationships block of a resource.
:authored_posts instead of just :posts)references in AshPostgres)Ash.Type.Union; see the “Polymorphic Relationships” guide for more information.relationships do
# belongs_to - adds foreign key to source resource
belongs_to :owner, MyApp.User do
allow_nil? false
attribute_type :integer # defaults to :uuid
end
# has_one - foreign key on destination resource
has_one :profile, MyApp.Profile
# has_many - foreign key on destination resource, returns list
has_many :posts, MyApp.Post do
filter expr(published == true)
sort published_at: :desc
end
# many_to_many - requires join resource
many_to_many :tags, MyApp.Tag do
through MyApp.PostTag
source_attribute_on_join_resource :post_id
destination_attribute_on_join_resource :tag_id
end
end
The join resource must be defined separately:
defmodule MyApp.PostTag do
use Ash.Resource,
data_layer: AshPostgres.DataLayer
attributes do
uuid_primary_key :id
# Add additional attributes if you need metadata on the relationship
attribute :added_at, :utc_datetime_usec do
default &DateTime.utc_now/0
end
end
relationships do
belongs_to :post, MyApp.Post, primary_key?: true, allow_nil?: false
belongs_to :tag, MyApp.Tag, primary_key?: true, allow_nil?: false
end
actions do
defaults [:read, :destroy, create: :*, update: :*]
end
end
# Using code interface options (preferred)
post = MyDomain.get_post!(id, load: [:author, comments: [:author]])
# Complex loading with filters
posts = MyDomain.list_posts!(
query: [load: [comments: [filter: [is_approved: true], limit: 5]]]
)
# Manual query building (for complex cases)
MyApp.Post
|> Ash.Query.load(comments: MyApp.Comment |> Ash.Query.filter(is_approved == true))
|> Ash.read!()
# Loading on existing records
Ash.load!(post, :author)
Prefer to use the strict? option when loading to only load necessary fields on related data.
MyApp.Post
|> Ash.Query.load([comments: [:title]], strict?: true)
There are two primary ways to manage relationships in Ash:
change manage_relationship/2-3 in ActionsUse this when input comes from action arguments:
actions do
update :update do
# Define argument for the related data
argument :comments, {:array, :map} do
allow_nil? false
end
argument :new_tags, {:array, :map}
# Link argument to relationship management
change manage_relationship(:comments, type: :append)
# For different argument and relationship names
change manage_relationship(:new_tags, :tags, type: :append)
end
end
Ash.Changeset.manage_relationship/3-4 in Custom ChangesUse this when building values programmatically:
defmodule MyApp.Changes.AssignTeamMembers do
use Ash.Resource.Change
def change(changeset, _opts, context) do
members = determine_team_members(changeset, context.actor)
Ash.Changeset.manage_relationship(
changeset,
:members,
members,
type: :append_and_remove
)
end
end
:append - Add new related records, ignore existing:append_and_remove - Add new related records, remove missing:remove - Remove specified related records:direct_control - Full CRUD control (create/update/destroy):create - Only create new recordson_lookup: :relate - Look up and relate existing recordson_no_match: :create - Create if no match foundon_match: :update - Update existing matcheson_missing: :destroy - Delete records not in inputvalue_is_key: :name - Use field as key for simple valuesFor comprehensive documentation, see the Managing Relationships section.
Creating a post with tags:
MyDomain.create_post!(%{
title: "New Post",
body: "Content here...",
tags: [%{name: "elixir"}, %{name: "ash"}] # Creates new tags
})
# Updating a post to replace its tags
MyDomain.update_post!(post, %{
tags: [tag1.id, tag2.id] # Replaces tags with existing ones by ID
})
Use mix ash.gen.* tasks as a basis for code generation when possible. Check the task docs with mix help <task>.
Be sure to use --yes to bypass confirmation prompts. Use --yes --dry-run to preview the changes.
Data layers determine how resources are stored and retrieved. Examples of data layers:
AshPostgres)Ash.DataLayer.Ets)Ash.DataLayer.Mnesia)data_layer: :embedded) (typically JSON under the hood)Specify a data layer when defining a resource:
defmodule MyApp.Post do
use Ash.Resource,
domain: MyApp.Blog,
data_layer: AshPostgres.DataLayer
postgres do
table "posts"
repo MyApp.Repo
end
# ... attributes, relationships, etc.
end
For embedded resources:
defmodule MyApp.Address do
use Ash.Resource,
data_layer: :embedded
attributes do
attribute :street, :string
attribute :city, :string
attribute :state, :string
attribute :zip, :string
end
end
Each data layer has its own configuration options and capabilities. Refer to the rules & documentation of the specific data layer package for more details.
After creating or modifying Ash code, run mix ash.codegen <short_name_describing_changes> to ensure any required additional changes are made (like migrations are generated). The name of the migration should be lower_snake_case. In a longer running dev session it's usually better to use mix ash.codegen --dev as you go and at the end run the final codegen with a sensible name describing all the changes made in the session.
authorize?: falseactor option# Good
Post
|> Ash.Query.for_read(:read, %{}, actor: current_user)
|> Ash.read!()
# BAD, DO NOT DO THIS
Post
|> Ash.Query.for_read(:read, %{})
|> Ash.read!(actor: current_user)
To use policies, add the Ash.Policy.Authorizer to your resource:
defmodule MyApp.Post do
use Ash.Resource,
domain: MyApp.Blog,
authorizers: [Ash.Policy.Authorizer]
# Rest of resource definition...
end
Policies determine what actions on a resource are permitted for a given actor. Define policies in the policies block:
policies do
# A simple policy that applies to all read actions
policy action_type(:read) do
# Authorize if record is public
authorize_if expr(public == true)
# Authorize if actor is the owner
authorize_if relates_to_actor_via(:owner)
end
# A policy for create actions
policy action_type(:create) do
# Only allow active users to create records
forbid_unless actor_attribute_equals(:active, true)
# Ensure the record being created relates to the actor
authorize_if relating_to_actor(:owner)
end
end
Policies evaluate from top to bottom with the following logic:
the first check that yields a result determines the policy outcome
# WRONG - This is OR logic, not AND logic!
policy action_type(:update) do
authorize_if actor_attribute_equals(:admin?, true) # If this passes, policy passes
authorize_if relates_to_actor_via(:owner) # Only checked if first fails
end
To require BOTH conditions in that example, you would use forbid_unless for the first condition:
# CORRECT - This requires BOTH conditions
policy action_type(:update) do
forbid_unless actor_attribute_equals(:admin?, true) # Must be admin
authorize_if relates_to_actor_via(:owner) # AND must be owner
end
Alternative patterns for AND logic:
expr(condition1 and condition2)forbid_unless for required conditions, then authorize_if for the final checkUse bypass policies to allow certain actors to bypass other policy restrictions. This should be used almost exclusively for admin bypasses.
policies do
# Bypass policy for admins - if this passes, other policies don't need to pass
bypass actor_attribute_equals(:admin, true) do
authorize_if always()
end
# Regular policies follow...
policy action_type(:read) do
# ...
end
end
Field policies control access to specific fields (attributes, calculations, aggregates):
field_policies do
# Only supervisors can see the salary field
field_policy :salary do
authorize_if actor_attribute_equals(:role, :supervisor)
end
# Allow access to all other fields
field_policy :* do
authorize_if always()
end
end
There are two main types of checks used in policies:
You can use built-in checks or create custom ones:
# Built-in checks
authorize_if actor_attribute_equals(:role, :admin)
authorize_if relates_to_actor_via(:owner)
authorize_if expr(public == true)
# Custom check module
authorize_if MyApp.Checks.ActorHasPermission
Create custom checks by implementing Ash.Policy.SimpleCheck or Ash.Policy.FilterCheck:
# Simple check - returns true/false
defmodule MyApp.Checks.ActorHasRole do
use Ash.Policy.SimpleCheck
def match?(%{role: actor_role}, _context, opts) do
actor_role == (opts[:role] || :admin)
end
def match?(_, _, _), do: false
end
# Filter check - returns query filter
defmodule MyApp.Checks.VisibleToUserLevel do
use Ash.Policy.FilterCheck
def filter(actor, _authorizer, _opts) do
expr(visibility_level <= ^actor.user_level)
end
end
# Usage
policy action_type(:read) do
authorize_if {MyApp.Checks.ActorHasRole, role: :manager}
authorize_if MyApp.Checks.VisibleToUserLevel
end
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:
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 use Ash expressions and can be pushed down to the data layer when possible:
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
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:
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.
For complex calculations, create a module that implements Ash.Resource.Calculation:
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
You can define calculations that accept arguments:
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 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)
Define calculation functions on your domain for standalone use:
# 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"
Aggregates allow you to retrieve summary information over groups of related data, like counts, sums, or averages. Define aggregates in the aggregates block of a resource.
Aggregates can work over relationships or directly over unrelated resources:
aggregates do
# Related aggregates - use relationship path
count :published_post_count, :posts do
filter expr(published == true)
end
sum :total_sales, :orders, :amount
exists :is_admin, :roles do
filter expr(name == "admin")
end
# Unrelated aggregates - use resource module directly
count :matching_profiles_count, Profile do
filter expr(name == parent(name))
end
sum :total_report_score, Report, :score do
filter expr(author_name == parent(name))
end
exists :has_reports, Report do
filter expr(author_name == parent(name))
end
end
For unrelated aggregates, use parent/1 to reference fields from the source resource.
# Using code interface options (preferred)
users = MyDomain.list_users!(
load: [:published_post_count, :total_sales],
query: [
filter: [published_post_count: [greater_than: 5]],
sort: [published_post_count: :desc]
]
)
# Manual query building (for complex cases)
User |> Ash.Query.filter(published_post_count > 5) |> Ash.read!()
# Loading on existing records
Ash.load!(users, :published_post_count)
For complex aggregates involving multiple relationships, use join filters:
aggregates do
sum :redeemed_deal_amount, [:redeems, :deal], :amount do
# Filter on the aggregate as a whole
filter expr(redeems.redeemed == true)
# Apply filters to specific relationship steps
join_filter :redeems, expr(redeemed == true)
join_filter [:redeems, :deal], expr(active == parent(require_active))
end
end
Use aggregates inline within expressions:
# Related inline aggregates
calculate :grade_percentage, :decimal, expr(
count(answers, query: [filter: expr(correct == true)]) * 100 /
count(answers)
)
# Unrelated inline aggregates
calculate :profile_count, :integer, expr(
count(Profile, filter: expr(name == parent(name)))
)
calculate :stats, :map, expr(%{
profiles: count(Profile, filter: expr(active == true)),
reports: count(Report, filter: expr(author_name == parent(name))),
has_active_profile: exists(Profile, active == true and name == parent(name))
})
Use exists/2 to check for the existence of records, either through relationships or unrelated resources:
# Check if user has any admin roles
Ash.Query.filter(User, exists(roles, name == "admin"))
# Check if post has comments with high scores
Ash.Query.filter(Post, exists(comments, score > 50))
# Check if any profile exists with the same name
Ash.Query.filter(User, exists(Profile, name == parent(name)))
# Check if user has any reports
Ash.Query.filter(User, exists(Report, author_name == parent(name)))
# Complex existence checks
Ash.Query.filter(User,
active == true and
exists(Profile, active == true and name == parent(name))
)
Unrelated exists expressions automatically apply authorization using the target resource's primary read action. Use parent/1 to reference fields from the source resource.
When testing resources:
Ash.TestAsh.can?authorize?: false in tests where authorization is not the focusAsh.GeneratorWhen running tests concurrently, using fixed values for identity attributes can cause deadlock errors. Multiple tests attempting to create records with the same unique values will conflict.
Always use globally unique values for identity attributes in tests:
# BAD - Can cause deadlocks in concurrent tests
%{email: "test@example.com", username: "testuser"}
# GOOD - Use globally unique values
%{
email: "test-#{System.unique_integer([:positive])}@example.com",
username: "user_#{System.unique_integer([:positive])}",
slug: "post-#{System.unique_integer([:positive])}"
}
For better organization, create a generator module:
defmodule MyApp.TestGenerators do
use Ash.Generator
def user(opts \\ []) do
changeset_generator(
User,
:create,
defaults: [
email: "user-#{System.unique_integer([:positive])}@example.com",
username: "user_#{System.unique_integer([:positive])}"
],
overrides: opts
)
end
end
# In your tests
test "concurrent user creation" do
users = MyApp.TestGenerators.generate_many(user(), 10)
# Each user has unique identity attributes
end
This applies to ANY field used in identity constraints, not just primary keys. Using globally unique values prevents frustrating intermittent test failures in CI environments.