Files
livebook/ash_tutorial/customizing_actions.livemd
2025-09-29 19:25:33 +08:00

278 lines
6.8 KiB
Markdown

# Ash: 5 - Customizing Actions
```elixir
Application.put_env(:ash, :validate_domain_resource_inclusion?, false)
Application.put_env(:ash, :validate_domain_config_inclusion?, false)
Mix.install([{:ash, "~> 3.0"}], consolidate_protocols: false)
```
## Customizing Actions
<div class="flex items-center w-full flex-start justify-between rounded-xl p-4" style="background-color: #f0f5f9; color: #61758a;">
<div class="flex">
<i class="ri-arrow-left-fill"></i>
<a class="flex ml-2" style="color: #61758a;" href="attributes.livemd">Attributes</a>
</div>
<div class="flex">
<i class="ri-home-fill"></i>
<a class="flex ml-2" style="color: #61758a;" href="overview.livemd">Home</a>
</div>
<div class="flex">
<a class="flex mr-2" style="color: #61758a;" href="relationships.livemd">Relationships</a>
<i class="ri-arrow-right-fill"></i>
</div>
</div>
### In this tutorial you will add custom Actions on the Ticket resource
Create 2 custom actions, `:open` and `:close`.
Custom actions allow you to attach *semantics* to actions.
* Instead of *Creating* a ticket, you can *Open* a ticket.
* Instead of *Updating* the status on a ticket you can *Close* it.
In addition, you can customize the behaviour of these actions. For example, closing a ticket only sets the status to `:closed`.
It also allows you to define what attributes can be set when calling that action. For example for the `:open` action, you can only allow the `:subject` and `:description` to be set.
It does not make sense to set the `:status` in this case as it should always be `:open`. For the closing action you won't allow any attributes to be set.
Custom actions go inside the `actions do ... end` block.
To define the `:open` action, open a `do end` block like so:
<!-- livebook:{"force_markdown":true} -->
```elixir
create :open do
end
```
Same for the `:close` action, but instead of `create`, use `update`.
You then can define the accepted attributes like so:
`accept [:subject, :description]`
Or in the case of the `:close` action:
`accept []`
Then for the `:close` action define a `change`:
`change set_attribute(:status, :closed)`
That's it, you defined your first custom actions.
<details class="rounded-lg my-4" style="background-color: #96ef86; color: #040604;">
<summary class="cursor-pointer font-bold p-4"></i>Show Solution</summary>
<div class="p-4">
```elixir
defmodule Tutorial.Support.Ticket do
use Ash.Resource,
domain: Tutorial.Support,
data_layer: Ash.DataLayer.Ets
actions do
defaults [:read]
create :open do
# By default you can provide all public attributes to an action
# This action should only accept the subject
accept [:subject, :description]
end
update :close do
# We don't want to accept any input here
accept []
change set_attribute(:status, :closed)
# A custom change could be added like so:
#
# change MyCustomChange
# change {MyCustomChange, opt: :val}
end
end
attributes do
uuid_primary_key :id
attribute :subject, :string, allow_nil?: false
attribute :description, :string
attribute :status, :atom do
constraints [one_of: [:open, :closed]]
default :open
allow_nil? false
end
create_timestamp :created_at
update_timestamp :updated_at
end
end
defmodule Tutorial.Support do
use Ash.Domain
resources do
resource Tutorial.Support.Ticket
end
end
```
</div>
</details>
### Enter your solution
```elixir
defmodule Tutorial.Support.Ticket do
use Ash.Resource,
domain: Tutorial.Support,
data_layer: Ash.DataLayer.Ets
actions do
defaults [:read]
# <-- Add the :open and :close action
end
attributes do
uuid_primary_key(:id)
attribute :subject, :string, allow_nil?: false
attribute :description, :string
attribute :status, :atom do
constraints one_of: [:open, :closed]
default :open
allow_nil? false
end
create_timestamp :created_at
update_timestamp :updated_at
end
end
defmodule Tutorial.Support do
use Ash.Domain
resources do
resource Tutorial.Support.Ticket
end
end
```
## Open a Ticket
Open a ticket.
Remember, when creating a resource, use a changeset (`Ash.Changeset.for_create/3`), which gets passed to `Ash.create!/1`. But in this case use the `:open` argument instead of `:create`.
<details class="rounded-lg my-4" style="background-color: #96ef86; color: #040604;">
<summary class="cursor-pointer font-bold p-4"></i>Show Solution</summary>
<div class="p-4">
```elixir
Tutorial.Support.Ticket
|> Ash.Changeset.for_create(:open, %{subject: "My Subject"})
|> Ash.create!()
```
</div>
</details>
Try setting the `:status` to `:closed` and see if it works.
<details class="rounded-lg my-4" style="background-color: #96ef86; color: #040604;">
<summary class="cursor-pointer font-bold p-4"></i>Show Solution</summary>
<div class="p-4">
```elixir
Tutorial.Support.Ticket
|> Ash.Changeset.for_create(:open, %{subject: "My Subject", status: :closed})
|> Ash.create!()
```
</div>
</details>
The output when trying to set `:status` should look something like this:
```
** (Ash.Error.Invalid) Input Invalid
* Invalid value provided for status: cannot be changed.
```
This is because you set the accepted attributes to `:subject` and `:description` only.
**Enter your solution**
```elixir
```
Create a Ticket and store it in the `ticket` variable.
<details class="rounded-lg my-4" style="background-color: #96ef86; color: #040604;">
<summary class="cursor-pointer font-bold p-4"></i>Show Solution</summary>
<div class="p-4">
```elixir
ticket =
Tutorial.Support.Ticket
|> Ash.Changeset.for_create(:open, %{subject: "My Subject"})
|> Ash.create!()
```
</div>
</details>
**Enter your solution**
```elixir
```
## Close a Ticket
Close the `ticket` you created in the previous section.
Remember to use `Ash.Changeset.for_update/2` with the `:close` action.
To update use `Ash.update!/1`.
<details class="rounded-lg my-4" style="background-color: #96ef86; color: #040604;">
<summary class="cursor-pointer font-bold p-4"></i>Show Solution</summary>
<div class="p-4">
```elixir
ticket
|> Ash.Changeset.for_update(:close)
|> Ash.update!()
```
</div>
</details>
```elixir
```
<div class="flex items-center w-full flex-start justify-between rounded-xl p-4" style="background-color: #f0f5f9; color: #61758a;">
<div class="flex">
<i class="ri-arrow-left-fill"></i>
<a class="flex ml-2" style="color: #61758a;" href="attributes.livemd">Attributes</a>
</div>
<div class="flex">
<i class="ri-home-fill"></i>
<a class="flex ml-2" style="color: #61758a;" href="overview.livemd">Home</a>
</div>
<div class="flex">
<a class="flex mr-2" style="color: #61758a;" href="relationships.livemd">Relationships</a>
<i class="ri-arrow-right-fill"></i>
</div>
</div>