add ash_tutorial
This commit is contained in:
9
ash_tutorial/README.MD
Normal file
9
ash_tutorial/README.MD
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Get Started
|
||||||
|
|
||||||
|
To get started you need a running instance of [Livebook](https://livebook.dev/)
|
||||||
|
|
||||||
|
[](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fwoutdp%2Fash_tutorial%2Fblob%2Fmaster%2Foverview.livemd)
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
Anything missing? Any errors? Tutorials out of date? Contributions are welcome!
|
||||||
93
ash_tutorial/actions.livemd
Normal file
93
ash_tutorial/actions.livemd
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Ash: 2 - 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is an Action?
|
||||||
|
|
||||||
|
<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="resources.livemd">Resources</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="querying.livemd">Querying</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- livebook:{"break_markdown":true} -->
|
||||||
|
|
||||||
|
Actions are much simpler: an action describes a <span title="Create, Read, Update, Delete">CRUD</span> operation that can be performed on a resource, but using whatever language makes the most sense for your given resource. For example, you may have a `publish` action on a `BlogPost` resource that changes the resource to be publicly visible. Under CRUD, this would be an `update` operation, but calling it the "publish action" more fully conveys what the operation does.
|
||||||
|
|
||||||
|
An action is defined on a resource in an `actions do ... end` block.
|
||||||
|
|
||||||
|
<!-- livebook:{"force_markdown":true} -->
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
actions do
|
||||||
|
create :create do
|
||||||
|
accept [:name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- livebook:{"break_markdown":true} -->
|
||||||
|
|
||||||
|
Adding that to our `Resource` module from earlier,
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Tutorial.Profile do
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: Tutorial
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
attribute :name, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
actions do
|
||||||
|
create :create do
|
||||||
|
accept [:name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Tutorial do
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource Tutorial.Profile
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
We can now use `for_create/3` to create a changeset.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Tutorial.Profile
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "My Name"})
|
||||||
|
```
|
||||||
|
|
||||||
|
<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="./resources.livemd">Resources</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="./querying.livemd">Querying</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
210
ash_tutorial/aggregates.livemd
Normal file
210
ash_tutorial/aggregates.livemd
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
# Ash: 8 - Aggregates
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Aggregates
|
||||||
|
|
||||||
|
<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="code_interfaces.livemd">Code Interfaces</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="calculations.livemd">Calculations</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### In this tutorial you will add an Aggregate on a Resource
|
||||||
|
|
||||||
|
[Aggregates](https://hexdocs.pm/ash/aggregates.html) in Ash allow for retrieving summary information over groups of related data. Aggregates can be either _count_, _first_, _sum_ or _list_.
|
||||||
|
|
||||||
|
Implement a count aggregate on the 'open tickets' a representative is assigned to.
|
||||||
|
|
||||||
|
First explore the comments in the code below, to make this work properly you have to add the inverse relationship of belongs_to inside the Representative resource.
|
||||||
|
|
||||||
|
To add an aggregate define an `aggregates do .. end` block inside the representative.
|
||||||
|
|
||||||
|
Inside the `aggregates` block, define a `count :count_of_open_tickets, :tickets do .. end` block.
|
||||||
|
|
||||||
|
Then inside this block, define a filter like so: `filter expr(status == :open)`
|
||||||
|
|
||||||
|
Also, notice what happens when you don't define a filter.
|
||||||
|
|
||||||
|
<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
|
||||||
|
aggregates do
|
||||||
|
count :count_of_open_tickets, :tickets do
|
||||||
|
filter expr(status == :open)
|
||||||
|
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]
|
||||||
|
|
||||||
|
# On creation set the representative by providing the id
|
||||||
|
create :open do
|
||||||
|
accept [:subject, :description, :representative_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
update :close do
|
||||||
|
accept []
|
||||||
|
change set_attribute(:status, :closed)
|
||||||
|
end
|
||||||
|
|
||||||
|
update :assign do
|
||||||
|
accept [:representative_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key(:id)
|
||||||
|
attribute :subject, :string, allow_nil?: false
|
||||||
|
attribute :description, :string, allow_nil?: true
|
||||||
|
|
||||||
|
attribute :status, :atom do
|
||||||
|
constraints one_of: [:open, :closed]
|
||||||
|
default :open
|
||||||
|
allow_nil? false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_timestamp :created_at
|
||||||
|
update_timestamp :updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
relationships do
|
||||||
|
belongs_to :representative, Tutorial.Support.Representative
|
||||||
|
end
|
||||||
|
|
||||||
|
code_interface do
|
||||||
|
define :assign, args: [:representative_id]
|
||||||
|
# <- added representative_id
|
||||||
|
define :open, args: [:subject, :description, :representative_id]
|
||||||
|
define :close, args: []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Tutorial.Support.Representative do
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: Tutorial.Support,
|
||||||
|
data_layer: Ash.DataLayer.Ets
|
||||||
|
|
||||||
|
actions do
|
||||||
|
defaults [:read]
|
||||||
|
|
||||||
|
create :create do
|
||||||
|
accept [:name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
attribute :name, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
# Added the inverse relationship of belongs_to in the ticket.
|
||||||
|
# This way we can reference :tickets inside the aggregates.
|
||||||
|
relationships do
|
||||||
|
has_many :tickets, Tutorial.Support.Ticket
|
||||||
|
end
|
||||||
|
|
||||||
|
code_interface do
|
||||||
|
define :create, args: [:name]
|
||||||
|
end
|
||||||
|
|
||||||
|
# <- Add the aggregates here
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Tutorial.Support do
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource Tutorial.Support.Ticket
|
||||||
|
resource Tutorial.Support.Representative
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query the Aggregate
|
||||||
|
|
||||||
|
Use a [Bulk Create](https://hexdocs.pm/ash/create-actions.html#bulk-creates) to create 4 tickets, 3 of which are assigned to Joe.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Create a representative
|
||||||
|
joe = Tutorial.Support.Representative.create!("Joe Armstrong")
|
||||||
|
|
||||||
|
# Bulk create 4 tickets, 3 of which are assigned to Joe
|
||||||
|
[
|
||||||
|
%{subject: "I can't see my eyes!"},
|
||||||
|
%{subject: "I can't find my hand!", representative_id: joe.id},
|
||||||
|
%{subject: "My fridge is flying away!", representative_id: joe.id},
|
||||||
|
%{subject: "My bed is invisible!", representative_id: joe.id}
|
||||||
|
]
|
||||||
|
|> Ash.bulk_create(Tutorial.Support.Ticket, :open, return_records?: true)
|
||||||
|
```
|
||||||
|
|
||||||
|
Close the last ticket assigned to Joe.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
require Ash.Query
|
||||||
|
|
||||||
|
# Retrieve the last ticket
|
||||||
|
[last_ticket] =
|
||||||
|
Tutorial.Support.Ticket
|
||||||
|
|> Ash.Query.filter(representative_id == ^joe.id)
|
||||||
|
|> Ash.Query.sort(created_at: :desc)
|
||||||
|
|> Ash.Query.limit(1)
|
||||||
|
|> Ash.read!()
|
||||||
|
|
||||||
|
# Close the last ticket
|
||||||
|
Tutorial.Support.Ticket.close!(last_ticket)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
joe = Ash.load!(joe, [:count_of_open_tickets])
|
||||||
|
joe.count_of_open_tickets
|
||||||
|
```
|
||||||
|
|
||||||
|
The result should be __2__, as you opened 4 tickets, 3 were assigned to Joe, but the last assigned ticket was closed.
|
||||||
|
|
||||||
|
<!-- livebook:{"break_markdown":true} -->
|
||||||
|
|
||||||
|
<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="code_interfaces.livemd">Code Interfaces</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="calculations.livemd">Calculations</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
309
ash_tutorial/attributes.livemd
Normal file
309
ash_tutorial/attributes.livemd
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# Ash: 4 - Attributes
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
<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="querying.livemd">Querying</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="customizing_actions.livemd">Customizing Actions</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### In this tutorial you will add and configure Attributes to a Ticket resource
|
||||||
|
|
||||||
|
The Ticket resource will represent a helpdesk ticket.
|
||||||
|
|
||||||
|
It will have 3 main attributes.
|
||||||
|
|
||||||
|
* A `subject` or title
|
||||||
|
* A `description`
|
||||||
|
* A `status` which can be either `:open` or `:closed`
|
||||||
|
|
||||||
|
It will also have 2 attributes for keeping track of when the ticket was *created*, and when it was *last updated*.
|
||||||
|
|
||||||
|
#### Subject
|
||||||
|
|
||||||
|
You need to make sure the `subject` is always set, it's not possible to create a ticket without a subject. You can do this by setting `allow_nil?` to `false`. Like so: `attribute :subject, :string, allow_nil?: false`
|
||||||
|
|
||||||
|
#### Description
|
||||||
|
|
||||||
|
The `:description` is simple, it's a `:string` and it is allowed to be empty.
|
||||||
|
|
||||||
|
#### Status
|
||||||
|
|
||||||
|
`:status` is more complicated.
|
||||||
|
|
||||||
|
* It is of the type `:atom`
|
||||||
|
* It can only be the value `:open` or `:closed`
|
||||||
|
* By default it is `:open`
|
||||||
|
* And it can't be `nil`
|
||||||
|
|
||||||
|
Attributes are set in a `do end` block like so:
|
||||||
|
|
||||||
|
<!-- livebook:{"force_markdown":true} -->
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
attribute :status, :atom do
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
allow_nil? false
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
To set a constraint of values, you can use the `constraints` option, like so:
|
||||||
|
|
||||||
|
`constraints [one_of: [:open, :closed]]`
|
||||||
|
|
||||||
|
To set the default value, you use `default :open`.
|
||||||
|
|
||||||
|
#### Keeping track of Created and Updated
|
||||||
|
|
||||||
|
Ash provides [create_timestamp](https://hexdocs.pm/ash/dsl-ash-resource.html#attributes-create_timestamp) and [update_timestamp](https://hexdocs.pm/ash/dsl-ash-resource.html#attributes-update_timestamp) to keep track of when the Resource was first created, and when it was last updated.
|
||||||
|
|
||||||
|
Add the following to the attributes block:
|
||||||
|
|
||||||
|
<!-- livebook:{"force_markdown":true} -->
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
create_timestamp :created_at
|
||||||
|
update_timestamp :updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
<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 :create do
|
||||||
|
accept [:subject, :description, :status]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
|
||||||
|
attribute :subject, :string, allow_nil?: false
|
||||||
|
attribute :description, :string
|
||||||
|
|
||||||
|
# status is either `open` or `closed`.
|
||||||
|
attribute :status, :atom do
|
||||||
|
# Constraints allow you to provide extra rules for the value.
|
||||||
|
# The available constraints depend on the type
|
||||||
|
# See the documentation for each type to know what constraints are available
|
||||||
|
# Since atoms are generally only used when we know all of the values
|
||||||
|
# it provides a `one_of` constraint, that only allows those values
|
||||||
|
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]
|
||||||
|
|
||||||
|
create :create do
|
||||||
|
accept [:subject, :description, :status]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
|
||||||
|
# Add the attributes here -->
|
||||||
|
# - Subject
|
||||||
|
# - Description
|
||||||
|
# - Status
|
||||||
|
# - Create Timestamp
|
||||||
|
# - Update Timestamp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Tutorial.Support do
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource Tutorial.Support.Ticket
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a Ticket
|
||||||
|
|
||||||
|
Create a `Ticket` without any attributes.
|
||||||
|
|
||||||
|
Remember, when creating a resource use a changeset (`Ash.Changeset.for_create/3`), which gets passed to `Ash.create!/1`.
|
||||||
|
|
||||||
|
The output should look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
** (Ash.Error.Invalid) Input Invalid
|
||||||
|
|
||||||
|
* attribute subject is required
|
||||||
|
```
|
||||||
|
|
||||||
|
<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(:create, %{})
|
||||||
|
|> Ash.create!()
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now create a Ticket with a `subject`
|
||||||
|
|
||||||
|
<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(:create, %{subject: "This is the subject"})
|
||||||
|
|> Ash.create!()
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now create a ticket with the `status` set to `:closed`
|
||||||
|
|
||||||
|
<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(:create, %{subject: "This is the subject", status: :closed})
|
||||||
|
|> Ash.create!()
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now try creating a ticket with the status set to `:pending`. This should give an error because `:pending` is not a valid `status`.
|
||||||
|
|
||||||
|
<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(:create, %{subject: "This is the subject", status: :pending})
|
||||||
|
|> Ash.create!()
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Latest created Ticket
|
||||||
|
|
||||||
|
Since you added a creation date, you can now query on the latest created ticket.
|
||||||
|
|
||||||
|
First, sort using the `Ash.Query.sort/2` function by `Ash.Query.sort(created_at: :desc)`
|
||||||
|
|
||||||
|
Then limit the amount of records with the `Ash.Query.limit/2` function by doing `Ash.Query.limit(1)`
|
||||||
|
|
||||||
|
Finally call `Ash.read_one!()` on the query.
|
||||||
|
|
||||||
|
*Hint: Use a pipeline*
|
||||||
|
|
||||||
|
<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.Query.sort(created_at: :desc)
|
||||||
|
|> Ash.Query.limit(1)
|
||||||
|
|> Ash.read_one!()
|
||||||
|
```
|
||||||
|
|
||||||
|
</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="querying.livemd">Querying</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="customizing_actions.livemd">Customizing Actions</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
113
ash_tutorial/calculations.livemd
Normal file
113
ash_tutorial/calculations.livemd
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Ash: 9 - Calculations
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Calculations
|
||||||
|
|
||||||
|
<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="aggregates.livemd">Aggregates</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 hidden">
|
||||||
|
<a class="flex mr-2" style="color: #61758a;" href="domain.livemd">Domain</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[Calculations](https://hexdocs.pm/ash/calculations.html) in Ash allow for displaying complex values as a top level value of a resource.
|
||||||
|
|
||||||
|
Create a `full_name` calculation on the User resource.
|
||||||
|
|
||||||
|
You already have the `first_name` and `last_name` defined, you can get the `full_name` by concatenating them with a space in the middle.
|
||||||
|
|
||||||
|
First, add a `calculations do ... end` block.
|
||||||
|
|
||||||
|
Then inside this block, add `calculate :full_name, :string, expr(first_name <> " " <> last_name)`
|
||||||
|
|
||||||
|
<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
|
||||||
|
calculations do
|
||||||
|
calculate :full_name, :string, expr(first_name <> " " <> last_name)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Enter your solution
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Tutorial.Accounts.User do
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: Tutorial.Accounts,
|
||||||
|
data_layer: Ash.DataLayer.Ets
|
||||||
|
|
||||||
|
actions do
|
||||||
|
defaults([:read])
|
||||||
|
|
||||||
|
create :create do
|
||||||
|
accept([:first_name, :last_name])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key(:id)
|
||||||
|
attribute(:first_name, :string, allow_nil?: false)
|
||||||
|
attribute(:last_name, :string, allow_nil?: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
# <--- Add the calculations here
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Tutorial.Accounts do
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource(Tutorial.Accounts.User)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create a User
|
||||||
|
|
||||||
|
Create a User and load in the `full_name`
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Tutorial.Accounts.User
|
||||||
|
|> Ash.Changeset.for_create(:create, %{first_name: "Joe", last_name: "Armstrong"})
|
||||||
|
|> Ash.create!()
|
||||||
|
|> Ash.load!([:full_name])
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the most basic example of a calculation. Note that calculations allow you to do any arbitrary operation on the Resource. More on this inside the [documentation](https://hexdocs.pm/ash/calculations.html).
|
||||||
|
|
||||||
|
<!-- livebook:{"break_markdown":true} -->
|
||||||
|
|
||||||
|
<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="aggregates.livemd">Aggregates</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 hidden">
|
||||||
|
<a class="flex mr-2" style="color: #61758a;" href="domain.livemd">Domain</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
230
ash_tutorial/code_interfaces.livemd
Normal file
230
ash_tutorial/code_interfaces.livemd
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# Ash: 7 - Code Interfaces
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Interfaces
|
||||||
|
|
||||||
|
<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="relationships.livemd">Relationships</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="aggregates.livemd">Aggregates</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### In this tutorial you will add a Code Interface
|
||||||
|
|
||||||
|
In previous tutorials you always used `Changesets` directly to CRUD on resources.
|
||||||
|
While this is a perfectly valid way of handling CRUD operations, Ash provides [Code Interfaces](https://hexdocs.pm/ash/code-interfaces.html) to make this more ergonomic for you.
|
||||||
|
The code interface can be defined on the resources *or* the domain. In this example, we will define it on the domain.
|
||||||
|
|
||||||
|
You will add 3 code interfaces for:
|
||||||
|
|
||||||
|
* Creating a representative
|
||||||
|
* Opening a ticket
|
||||||
|
* Assigning a representative
|
||||||
|
|
||||||
|
For each resource you want to add a code interface to, inside that resource definition on the domain, you will place "definitions".
|
||||||
|
For example:
|
||||||
|
|
||||||
|
<!-- livebook:{"force_markdown":true} -->
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
resource MyApp.Tweet do
|
||||||
|
define :create_tweet, args: [:content]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- livebook:{"break_markdown":true} -->
|
||||||
|
|
||||||
|
Then add the 3 interfaces by defining the following inside the `Ticket` resource's block:
|
||||||
|
|
||||||
|
* `define :assign_ticket, args: [:representative_id], action: :assign`
|
||||||
|
* `define :open_ticket, args: [:subject, :description], action: :open`
|
||||||
|
|
||||||
|
And the following inside the `Representative` resource's block.
|
||||||
|
|
||||||
|
* `define :create_representative, args: [:name], action: :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
|
||||||
|
resource Tutorial.Support.Ticket do
|
||||||
|
define :assign_ticket, args: [:representative_id], action: :assign
|
||||||
|
define :open_ticket, args: [:subject, :description], action: :open
|
||||||
|
end
|
||||||
|
|
||||||
|
resource Tutorial.Support.Representative do
|
||||||
|
define :create_representative, args: [:name], action: :create
|
||||||
|
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]
|
||||||
|
|
||||||
|
create :open do
|
||||||
|
accept [:subject, :description]
|
||||||
|
end
|
||||||
|
|
||||||
|
update :close do
|
||||||
|
change set_attribute(:status, :closed)
|
||||||
|
end
|
||||||
|
|
||||||
|
update :assign do
|
||||||
|
accept [:representative_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
attribute :subject, :string, allow_nil?: false
|
||||||
|
attribute :description, :string, allow_nil?: true
|
||||||
|
|
||||||
|
attribute :status, :atom do
|
||||||
|
constraints one_of: [:open, :closed]
|
||||||
|
default :open
|
||||||
|
allow_nil? false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_timestamp :created_at
|
||||||
|
update_timestamp :updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
relationships do
|
||||||
|
belongs_to :representative, Tutorial.Support.Representative
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Tutorial.Support.Representative do
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: Tutorial.Support,
|
||||||
|
data_layer: Ash.DataLayer.Ets
|
||||||
|
|
||||||
|
actions do
|
||||||
|
defaults [:read]
|
||||||
|
|
||||||
|
create :create do
|
||||||
|
accept [:name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
attribute :name, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Tutorial.Support do
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource Tutorial.Support.Ticket do
|
||||||
|
# <-- Add ticket code interface here
|
||||||
|
end
|
||||||
|
|
||||||
|
resource Tutorial.Support.Representative do
|
||||||
|
# <-- Add representative code interface here
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using the Code Interfaces
|
||||||
|
|
||||||
|
Create a Representative, but instead of using a changeset, use the code interface you created.
|
||||||
|
|
||||||
|
You can use a code interface by calling `Tutorial.Support.create_representative!/1` with the desired name.
|
||||||
|
|
||||||
|
Set the name to `Joe Armstrong` and store it in a `joe` 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
|
||||||
|
joe = Tutorial.Support.create_representative!("Joe Armstrong")
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a Ticket with the created code interface.
|
||||||
|
|
||||||
|
Call `Tutorial.Support.open_ticket!/2` with a `subject` and `description` and store the result in a `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.open_ticket!("I can't find my hand!", "I think someone stole it.")
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Assign the Representative `joe` to the Ticket using the code Interface.
|
||||||
|
|
||||||
|
Use `Tutorial.Support.assign_ticket!/2`.
|
||||||
|
|
||||||
|
<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.assign_ticket!(ticket, joe.id)
|
||||||
|
```
|
||||||
|
|
||||||
|
</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="relationships.livemd">Managing Relationships</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="aggregates.livemd">Aggregates</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
277
ash_tutorial/customizing_actions.livemd
Normal file
277
ash_tutorial/customizing_actions.livemd
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# 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>
|
||||||
37
ash_tutorial/overview.livemd
Normal file
37
ash_tutorial/overview.livemd
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Ash: 0 - Overview
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
In this tutorial you will create Ash Resources, query on them and use ETS as your data storage. You won't be building an overarching application, instead you'll build Resources that introduce you to Ash concepts.
|
||||||
|
|
||||||
|
Each tutorial builds on top of the previous one, it is therefore recommended to start the tutorial **sequentially from the beginning** as each section introduces new concepts that will later be used.
|
||||||
|
|
||||||
|
The best way to follow the tutorial is reading the instructions in the tutorial, coming up with your own solution, and then **verifying with the proposed solution**. It is also recommended to **not copy paste** code.
|
||||||
|
|
||||||
|
The goal of this tutorial is to get you familiar with the basic concepts of Ash, and give you a good sense of what's possible. It serves as a primer for the [Ash documentation](https://hexdocs.pm/ash/get-started.html). Feel free to consult this documentation alongside the tutorial to learn more. That being said, the documenation shouldn't be necessary to complete the tutorial.
|
||||||
|
|
||||||
|
Have fun!
|
||||||
|
|
||||||
|
<!-- livebook:{"break_markdown":true} -->
|
||||||
|
|
||||||
|
1. [Resources](resources.livemd)
|
||||||
|
2. [Actions](actions.livemd)
|
||||||
|
3. [Querying](querying.livemd)
|
||||||
|
4. [Attributes](attributes.livemd)
|
||||||
|
5. [Customizing Actions](customizing_actions.livemd)
|
||||||
|
6. [Relationships](relationships.livemd)
|
||||||
|
7. [Code Interfaces](code_interfaces.livemd)
|
||||||
|
8. [Aggregates](aggregates.livemd)
|
||||||
|
9. [Calculations](calculations.livemd)
|
||||||
|
|
||||||
|
## Contributing?
|
||||||
|
|
||||||
|
_Anything missing? Any errors? Tutorials out of date? [Contributions are welcome!](https://github.com/ash-project/ash_tutorial)_
|
||||||
|
|
||||||
|
_When contributing, please follow the guidelines in [diataxis](https://diataxis.fr/tutorials/)._
|
||||||
271
ash_tutorial/querying.livemd
Normal file
271
ash_tutorial/querying.livemd
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# Ash: 3 - Querying
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## But First, Data Persistence
|
||||||
|
|
||||||
|
<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="actions.livemd">Actions</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="attributes.livemd">Attributes</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- livebook:{"break_markdown":true} -->
|
||||||
|
|
||||||
|
So far we have just created changesets, but we have not actually created any instances of a resource, also known as records. Before we can play around with querying, we need to add a data layer. It is much more common to use [Postgres](https://github.com/ash-project/ash_postgres) in production, but we will use [ETS data layer](https://hexdocs.pm/ash/dsl-ash-datalayer-ets.html#ets) for the remainder of this tutorial to keep dependencies to a minimum.
|
||||||
|
|
||||||
|
<!-- livebook:{"force_markdown":true} -->
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: YourDomain,
|
||||||
|
data_layer: Ash.DataLayer.Ets
|
||||||
|
```
|
||||||
|
|
||||||
|
## In this tutorial you will do basic Query and CRUD operations
|
||||||
|
|
||||||
|
But first you need to enable all basic **CRUD** operations.
|
||||||
|
|
||||||
|
Do this by adding default `:read` and `:destroy` actions, as well as `:create` and `:update` actions that accept `[:name]`.
|
||||||
|
|
||||||
|
<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.Profile do
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: Tutorial,
|
||||||
|
data_layer: Ash.DataLayer.Ets
|
||||||
|
|
||||||
|
actions do
|
||||||
|
defaults [:read, :destroy]
|
||||||
|
|
||||||
|
create :create do
|
||||||
|
accept [:name]
|
||||||
|
end
|
||||||
|
|
||||||
|
update :update do
|
||||||
|
accept [:name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
attribute :name, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Tutorial do
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource Tutorial.Profile
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Enter your solution
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Tutorial.Profile do
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: Tutorial,
|
||||||
|
data_layer: Ash.DataLayer.Ets
|
||||||
|
|
||||||
|
actions do
|
||||||
|
# <--- Create your actions here
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
attribute :name, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Tutorial do
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource Tutorial.Profile
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating
|
||||||
|
|
||||||
|
Add 2 Profiles to the database.
|
||||||
|
|
||||||
|
* One with the name "Joe Armstrong"
|
||||||
|
* One with your name
|
||||||
|
|
||||||
|
You can create a `Profile` by:
|
||||||
|
|
||||||
|
* Creating a Changeset with `Ash.Changeset.for_create(Tutorial.Profile, :create, %{name: "Your Name"})`,
|
||||||
|
* Then giving the changeset to `Ash.create!()`.
|
||||||
|
|
||||||
|
*Hint: Use a pipeline*
|
||||||
|
|
||||||
|
<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.Profile
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "The Name"})
|
||||||
|
|> Ash.create!()
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
**Enter your solution**
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reading
|
||||||
|
|
||||||
|
Now, read all the generated Profiles.
|
||||||
|
|
||||||
|
Call `Ash.read!/1` with the `Tutorial.Profile` module.
|
||||||
|
|
||||||
|
<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.Profile
|
||||||
|
|> Ash.read!()
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
**Enter your solution**
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Now fetch the "Joe Armstrong" Profile.
|
||||||
|
|
||||||
|
You can achieve this by introducing a filter.
|
||||||
|
|
||||||
|
First you'll need to `require Ash.Query`
|
||||||
|
|
||||||
|
Then call `Ash.Query.filter(name == "Joe Armstrong")` with the `Tutorial.Profile`.
|
||||||
|
|
||||||
|
Put the result into the `joe` variable. `Ash.read!/1` returns a list, so make sure to extract the single returned value out of that list.
|
||||||
|
|
||||||
|
<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
|
||||||
|
require Ash.Query
|
||||||
|
|
||||||
|
[joe] =
|
||||||
|
Tutorial.Profile
|
||||||
|
|> Ash.Query.filter(name == "Joe Armstrong")
|
||||||
|
|> Ash.read!()
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
**Enter your solution**
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll use the `joe` variable in the next sections.
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
Now change `Joe Armstrong`'s name to `Neil Armstrong`.
|
||||||
|
|
||||||
|
You can do this by providing the `Ash.Changeset.for_update/3` with:
|
||||||
|
|
||||||
|
* the resource you want to change, in this case `joe`
|
||||||
|
* the `:update` atom
|
||||||
|
* a map of the values you want to change, in this case `%{name: "Neil Armstrong"}`
|
||||||
|
|
||||||
|
Then apply the changeset by calling `Ash.update!/1` with the changeset.
|
||||||
|
|
||||||
|
*Hint: Using a pipeline might be a good idea.*
|
||||||
|
|
||||||
|
<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
|
||||||
|
joe
|
||||||
|
|> Ash.Changeset.for_update(:update, %{name: "Neil Armstrong"})
|
||||||
|
|> Ash.update!()
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
**Enter your solution**
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Destroying
|
||||||
|
|
||||||
|
Finally, remove `joe` from the database.
|
||||||
|
|
||||||
|
Do this using the `Ash.destroy!/1` function.
|
||||||
|
|
||||||
|
<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
|
||||||
|
Ash.destroy!(joe)
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
**Enter your solution**
|
||||||
|
|
||||||
|
```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="actions.livemd">Actions</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="attributes.livemd">Attributes</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
163
ash_tutorial/relationships.livemd
Normal file
163
ash_tutorial/relationships.livemd
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Ash: 6 - Relationships
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
<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="customizing_actions.livemd">Customizing Actions</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="code_interfaces.livemd">Code Interfaces</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### In this tutorial you will create a relationship between a Ticket and Representative resource
|
||||||
|
|
||||||
|
Create a `relationships do .. end` block in the Ticket resource.
|
||||||
|
|
||||||
|
Inside the `relationships` block, define a `belongs_to` representative like so:
|
||||||
|
|
||||||
|
`belongs_to :representative, Tutorial.Support.Representative`
|
||||||
|
|
||||||
|
<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
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
relationships do
|
||||||
|
# belongs_to means that the destination attribute is unique, meaning only one related record could exist.
|
||||||
|
# We assume that the destination attribute is `representative_id` based
|
||||||
|
# on the name of this relationship and that the source attribute is `representative_id`.
|
||||||
|
# We create `representative_id` automatically.
|
||||||
|
belongs_to :representative, Tutorial.Support.Representative
|
||||||
|
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]
|
||||||
|
|
||||||
|
create :create do
|
||||||
|
accept [:subject, :description, :status]
|
||||||
|
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
|
||||||
|
|
||||||
|
# <-- Add the relationship here
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Tutorial.Support.Representative do
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: Tutorial.Support,
|
||||||
|
data_layer: Ash.DataLayer.Ets
|
||||||
|
|
||||||
|
actions do
|
||||||
|
defaults [:read]
|
||||||
|
|
||||||
|
create :create do
|
||||||
|
accept [:name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key(:id)
|
||||||
|
attribute(:name, :string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Tutorial.Support do
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource Tutorial.Support.Ticket
|
||||||
|
resource Tutorial.Support.Representative
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a Ticket record with a representative Relationship
|
||||||
|
|
||||||
|
First, create the representative Joe.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Creates a Representative
|
||||||
|
joe =
|
||||||
|
Tutorial.Support.Representative
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "Joe Armstrong"})
|
||||||
|
|> Ash.create!()
|
||||||
|
```
|
||||||
|
|
||||||
|
Next up, create a ticket and assign the representative Joe to the ticket.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Creates a Ticket with the representative
|
||||||
|
Tutorial.Support.Ticket
|
||||||
|
|> Ash.Changeset.for_create(:create, %{subject: "My spoon is too big!", representative_id: joe.id})
|
||||||
|
|> Ash.create!()
|
||||||
|
# `load!/2` loads in the representative. Try removing this line to see what changes
|
||||||
|
|> Ash.load!([:representative])
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see it didn't quite work. The `representative_id` is not accepted by the create action on `Tutorial.Support.Ticket`. If you add `:representative_id` to that list,
|
||||||
|
and try again you should now see that the representative was correctly assigned.
|
||||||
|
|
||||||
|
<!-- livebook:{"break_markdown":true} -->
|
||||||
|
|
||||||
|
<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="customizing_actions.livemd">Customizing Actions</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="code_interfaces.livemd">Code Interfaces</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
79
ash_tutorial/resources.livemd
Normal file
79
ash_tutorial/resources.livemd
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Ash: 1 - Resources
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is a resource?
|
||||||
|
|
||||||
|
<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-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="actions.livemd">Actions</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- livebook:{"break_markdown":true} -->
|
||||||
|
|
||||||
|
The fundamental building blocks of Ash are resources, actions, and domains. The resource is Ash's central concept, so we need to define it well for anything else to make sense, so let's turn to the [Ash Glossary](https://hexdocs.pm/ash/glossary.html#resource).
|
||||||
|
|
||||||
|
> The central concept in Ash, a resource is a domain model object in your system, the nouns that your app revolves around. Resources contain definitions of the data they hold in the form of attributes, but also define actions that can be taken on that data and actors that are allowed to run them.
|
||||||
|
>
|
||||||
|
> It is not a strict requirement that resources contain data - they can be used purely to create a standard interface for performing tasks - but in practice, most resources will be used to manage data.
|
||||||
|
|
||||||
|
A domain simply groups together related resources. Some things can be defined in a domain instead of a resource. We will do this when we get to code interfaces later on.
|
||||||
|
|
||||||
|
## Example Code
|
||||||
|
|
||||||
|
Here we have a minimal resource called `Profile` that has, aside from its primary key, a single string attribute `name`.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Tutorial.Profile do
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: Tutorial
|
||||||
|
|
||||||
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
attribute :name, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
To be able to do anything with this resource, we also need a domain.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Tutorial do
|
||||||
|
use Ash.Domain
|
||||||
|
|
||||||
|
resources do
|
||||||
|
resource Tutorial.Profile
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can create a changeset for the resource.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Tutorial.Profile
|
||||||
|
|> Ash.Changeset.new()
|
||||||
|
|> Ash.Changeset.change_attributes(%{name: "John Doe"})
|
||||||
|
```
|
||||||
|
|
||||||
|
Changesets are used to create and update data in Ash. (**Note:** without a data layer, you won't be able to persist creations and changes. This will be shown in a later tutorial.)
|
||||||
|
|
||||||
|
<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-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="actions.livemd">Actions</a>
|
||||||
|
<i class="ri-arrow-right-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
59
elixir_koan.livemd
Normal file
59
elixir_koan.livemd
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# elixir_koan
|
||||||
|
|
||||||
|
## Section
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Integer.parse("2A", 16)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Integer.parse("5 years")
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Integer.parse("1.2")
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.parse("34.5")
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.parse("1.5 million dollars")
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.ceil(34.25)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.ceil(34.25, 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.floor(99.99)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.floor(12.345,2)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.round(5.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.round(5.4)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.round(8.94, 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Float.round(-5.5674, 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
tuple_size({:a, :b, :c})
|
||||||
|
```
|
||||||
@@ -24,3 +24,11 @@ Kernel./(10, 3)
|
|||||||
```elixir
|
```elixir
|
||||||
Kernel.*(10, 3)
|
Kernel.*(10, 3)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
:simple != :easy
|
||||||
|
```
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
:hello == :world
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user