| name | ash-code-interfaces |
| description | Define and use code interfaces for calling Ash resources from domains |
Code Interfaces
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)
Authorization Functions
For each action defined in a code interface, Ash automatically generates corresponding authorization check functions:
can_action_name?(actor, params \\ %{}, opts \\ [])- Returnstrue/falsefor 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.