--- name: ash description: Rules for working with Ash --- # Rules for working with Ash ## Understanding 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. ## Code Structure & Organization - Organize code around domains and resources - Each resource should be focused and well-named - Create domain-specific actions rather than generic CRUD operations - Put business logic inside actions rather than in external modules - Use resources to model your domain entities ## Code Interfaces Use code interfaces on domains to define the contract for calling into Ash resources. See the [Code interface guide for more](https://hexdocs.pm/ash/code-interfaces.html). Define code interfaces on the domain, like this: ```elixir resource ResourceName do define :fun_name, action: :action_name end ``` For more complex interfaces with custom transformations: ```elixir 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. ```elixir 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: ```elixir resource ResourceName do define :create, action: :action_name, args: [:field1] end ``` ```elixir Domain.create!(field1_value, %{field2: field2_value}, actor: current_user) ``` You should generally prefer using this map of extra inputs over defining optional arguments. ```elixir # 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: ```elixir # 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: ```elixir # 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: ```elixir |> Ash.Changeset.before_transaction(fn changeset, context -> MyApp.ExternalService.reserve_inventory(changeset, scope: context) changeset end) ``` ### Authorization Functions 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 checks - `can_action_name(actor, params \\ %{}, opts \\ [])` - Returns `{:ok, true/false}` or `{:error, reason}` Example usage: ```elixir # 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. ## Actions - Create specific, well-named actions rather than generic ones - Put all business logic inside action definitions - Use hooks like `Ash.Changeset.after_action/2`, `Ash.Changeset.before_action/2` to add additional logic inside the same transaction. - Use hooks like `Ash.Changeset.after_transaction/2`, `Ash.Changeset.before_transaction/2` to add additional logic outside the transaction. - Use action arguments for inputs that need validation - Use preparations to modify queries before execution - Preparations support `where` clauses for conditional execution - Use `only_when_valid?` to skip preparations when the query is invalid - Use changes to modify changesets before execution - Use validations to validate changesets before execution - Prefer domain code interfaces to call actions instead of directly building queries/changesets and calling functions in the `Ash` module - A resource could be *only generic actions*. This can be useful when you are using a resource only to model behavior. ## Querying Data Use `Ash.Query` to build queries for reading data from your resources. The query module provides a declarative way to filter, sort, and load data. ## Ash.Query.filter is a macro **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` ### Common Query Operations - **Filter**: `Ash.Query.filter(query, field == value)` - **Sort**: `Ash.Query.sort(query, field: :asc)` - **Load relationships**: `Ash.Query.load(query, [:author, :comments])` - **Limit**: `Ash.Query.limit(query, 10)` - **Offset**: `Ash.Query.offset(query, 20)` ## Error Handling 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: 1. **Forbidden** (`Ash.Error.Forbidden`) - Occurs when a user attempts an action they don't have permission to perform 2. **Invalid** (`Ash.Error.Invalid`) - Occurs when input data doesn't meet validation requirements 3. **Framework** (`Ash.Error.Framework`) - Occurs when there's an issue with how Ash is being used 4. **Unknown** (`Ash.Error.Unknown`) - Occurs for unexpected errors that don't fit the other categories These 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. ### Using Validations 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_in` - `compare`, `confirm`, `match`, `negate`, `one_of`, `present`, `string_length` - Custom validations that implement the `supports/1` callback Common validation patterns: ```elixir # 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: ```elixir 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. ### Using Preparations 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: ```elixir # 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 ``` ```elixir 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 ``` - **Avoid redundant validations** - Don't add validations that duplicate attribute constraints: ```elixir # 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 ``` ### Using Changes 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: ```elixir # 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: ```elixir 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: ```elixir 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, []} ``` ## Custom Modules vs. Anonymous Functions Prefer to put code in its own module and refer to that in changes, preparations, validations etc. For example, prefer this: ```elixir 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 ``` ### Action Types - **Read**: For retrieving records - **Create**: For creating records - **Update**: For changing records - **Destroy**: For removing records - **Generic**: For custom operations that don't fit the other types ## Relationships Relationships describe connections between resources and are a core component of Ash. Define relationships in the `relationships` block of a resource. ### Best Practices for Relationships - Be descriptive with relationship names (e.g., use `:authored_posts` instead of just `:posts`) - Configure foreign key constraints in your data layer if they have them (see `references` in AshPostgres) - Always choose the appropriate relationship type based on your domain model #### Relationship Types - For Polymorphic relationships, you can model them using `Ash.Type.Union`; see the “Polymorphic Relationships” guide for more information. ```elixir 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: ```elixir 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 ``` ### Loading Relationships ```elixir # 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. ```elixir MyApp.Post |> Ash.Query.load([comments: [:title]], strict?: true) ``` ### Managing Relationships There are two primary ways to manage relationships in Ash: #### 1. Using `change manage_relationship/2-3` in Actions Use this when input comes from action arguments: ```elixir 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 ``` #### 2. Using `Ash.Changeset.manage_relationship/3-4` in Custom Changes Use this when building values programmatically: ```elixir 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 ``` #### Quick Reference - Management Types - `: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 records #### Quick Reference - Common Options - `on_lookup: :relate` - Look up and relate existing records - `on_no_match: :create` - Create if no match found - `on_match: :update` - Update existing matches - `on_missing: :destroy` - Delete records not in input - `value_is_key: :name` - Use field as key for simple values For comprehensive documentation, see the [Managing Relationships](https://hexdocs.pm/ash/relationships.html#managing-relationships) section. #### Examples Creating a post with tags: ```elixir 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 }) ``` ## Generating Code Use `mix ash.gen.*` tasks as a basis for code generation when possible. Check the task docs with `mix help `. Be sure to use `--yes` to bypass confirmation prompts. Use `--yes --dry-run` to preview the changes. ## Data Layers Data layers determine how resources are stored and retrieved. Examples of data layers: - **Postgres**: For storing resources in PostgreSQL (via `AshPostgres`) - **ETS**: For in-memory storage (`Ash.DataLayer.Ets`) - **Mnesia**: For distributed storage (`Ash.DataLayer.Mnesia`) - **Embedded**: For resources embedded in other resources (`data_layer: :embedded`) (typically JSON under the hood) - **Ash.DataLayer.Simple**: For resources that aren't persisted at all. Leave off the data layer, as this is the default. Specify a data layer when defining a resource: ```elixir 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: ```elixir 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. ## Migrations and Schema Changes After creating or modifying Ash code, run `mix ash.codegen ` 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. ## Authorization - When performing administrative actions, you can bypass authorization with `authorize?: false` - To run actions as a particular user, look that user up and pass it as the `actor` option - Always set the actor on the query/changeset/input, not when calling the action - Use policies to define authorization rules ```elixir # 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) ``` ### Policies To use policies, add the `Ash.Policy.Authorizer` to your resource: ```elixir defmodule MyApp.Post do use Ash.Resource, domain: MyApp.Blog, authorizers: [Ash.Policy.Authorizer] # Rest of resource definition... end ``` ### Policy Basics Policies determine what actions on a resource are permitted for a given actor. Define policies in the `policies` block: ```elixir 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 ``` ### Policy Evaluation Flow Policies evaluate from top to bottom with the following logic: 1. All policies that apply to an action must pass for the action to be allowed 2. Within each policy, checks evaluate from top to bottom 3. The first check that produces a decision determines the policy result 4. If no check produces a decision, the policy defaults to forbidden ### IMPORTANT: Policy Check Logic **the first check that yields a result determines the policy outcome** ```elixir # 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: ```elixir # 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: - Use multiple separate policies (each must pass independently) - Use a single complex expression with `expr(condition1 and condition2)` - Use `forbid_unless` for required conditions, then `authorize_if` for the final check ### Bypass Policies Use bypass policies to allow certain actors to bypass other policy restrictions. This should be used almost exclusively for admin bypasses. ```elixir 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 Field policies control access to specific fields (attributes, calculations, aggregates): ```elixir 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 ``` ### Policy Checks There are two main types of checks used in policies: 1. **Simple checks** - Return true/false answers (e.g., "is the actor an admin?") 2. **Filter checks** - Return filters to apply to data (e.g., "only show records owned by the actor") You can use built-in checks or create custom ones: ```elixir # 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 ``` #### Custom Policy Checks Create custom checks by implementing `Ash.Policy.SimpleCheck` or `Ash.Policy.FilterCheck`: ```elixir # 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 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" ``` ## Aggregates 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: ```elixir 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. ### Aggregate Types - **count**: Counts related items meeting criteria - **sum**: Sums a field across related items - **exists**: Returns boolean indicating if matching related items exist (also supports unrelated resources) - **first**: Gets the first related value matching criteria - **list**: Lists the related values for a specific field - **max**: Gets the maximum value of a field - **min**: Gets the minimum value of a field - **avg**: Gets the average value of a field ### Using Aggregates ```elixir # 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) ``` ### Join Filters For complex aggregates involving multiple relationships, use join filters: ```elixir 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 ``` ### Inline Aggregates Use aggregates inline within expressions: ```elixir # 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)) }) ``` ## Exists Expressions Use `exists/2` to check for the existence of records, either through relationships or unrelated resources: ### Related Exists ```elixir # 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)) ``` ### Unrelated Exists ```elixir # 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. ## Testing When testing resources: - Test your domain actions through the code interface - Use test utilities in `Ash.Test` - Test authorization policies work as expected using `Ash.can?` - Use `authorize?: false` in tests where authorization is not the focus - Write generators using `Ash.Generator` - Prefer to use raising versions of functions whenever possible, as opposed to pattern matching ### Preventing Deadlocks in Concurrent Tests When 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. #### Use Globally Unique Values Always use globally unique values for identity attributes in tests: ```elixir # 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])}" } ``` #### Creating Reusable Test Generators For better organization, create a generator module: ```elixir 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.